﻿{
  单元名称: MainForm.pas
  项目名称: HotKeyCapture - Windows 快捷键抓取工具
  
  功能描述:
    此单元实现了一个 Windows 快捷键扫抓取工具，能够实时检测系统中已被注册的快捷键组合并尝试
    识别注册这些快捷键的应用程序。该工具使用低级键盘钩子技术捕获用户按键，并利用 Windows 
    RegisterHotKey API 尝试临时注册快捷键组合以判断其占用状态。
  
  主要功能:
    1. 实时监控键盘输入，特别关注带有修饰键 (Ctrl/Alt/Shift/Win) 的组合键。
    2. 检测快捷键组合是否已被系统中其他应用程序注册使用。
    3. 使用启发式方法尝试识别可能注册快捷键的应用程序。
    4. 提供友好的 UI 界面显示快捷键状态和可能的注册者信息。
  
  技术实现:
    - 使用 Windows 低级键盘钩子 (WH_KEYBOARD_LL) 全局捕获键盘事件。
    - 通过 RegisterHotKey 和 UnregisterHotKey API 测试快捷键注册状态。
    - 利用 Windows  API 识别可能的快捷键注册进程。
    - 采用线程安全的 UI 更新机制确保界面响应流畅。
  
  局限性:
    - 快捷键注册者识别采用启发式方法，准确性有限(基于前台窗口进程)。
    - 某些系统级快捷键可能无法被正确识别。
    - 在高安全级别系统下可能需要管理员权限才能安装全局钩子。
  
  环境需求:
    - Windows 10+ 操作系统(已在Windows 11上测试)。
  
  修改历史:
    2025-04-16 - 创建初始版本。
    2025-04-18 - 完善快捷键检测逻辑，增加注册者识别功能。
                 添加重试机制，提高钩子安装可靠性。
                 完善错误处理，增加详细的错误代码解释。
    2025-04-21 - 增加窗口标题获取功能，提供更详细的快捷键注册信息。
                 优化用户界面反馈，提升用户体验。
                 优化 GetProcessName 函数，使用 QueryFullProcessImageName 替换旧方法，降低权限需求。
}
unit MainForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TfrmMain = class(TForm)
    btnQuit: TButton;
    lblHotKey: TLabel;
    lblInfo: TLabel;
    procedure btnQuitClick(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

uses
  PsAPI, Winapi.TlHelp32;

type
  // KBDLLHOOKSTRUCT 是 Windows API 中一个重要的数据结构，用于在低级键盘钩子(WH_KEYBOARD_LL)中传递键盘事件的详细信息。
  KBDLLHOOKSTRUCT = record
    vkCode: DWORD;          // 虚拟键码
    scanCode: DWORD;        // 硬件扫描码
    flags: DWORD;           // 各种标志位
    time: DWORD;            // 消息时间戳
    dwExtraInfo: ULONG_PTR; // 额外信息
  end;
  PKBDLLHOOKSTRUCT = ^KBDLLHOOKSTRUCT;

  TModifierKey = (mkShift, mkCtrl, mkAlt, mkWin);
  TModifierKeys = set of TModifierKey;

const
  // Windows Vista 及更高版本引入的进程访问权限标志，当前 Delphi 版本对此缺乏定义。
  PROCESS_QUERY_LIMITED_INFORMATION = $1000;

// Windows API 中的 QueryFullProcessImageName 函数，当前 Delphi 版本对此缺乏定义。
function QueryFullProcessImageName(hProcess: THandle; dwFlags: DWORD;
  lpImageFileName: LPWSTR; var lpdwSize: DWORD): BOOL; stdcall;
  external 'kernel32.dll' name 'QueryFullProcessImageNameW';

var
  KeyboardHook: HHOOK = 0;

{
  过程名: UninstallKeyboardHook
  功能: 安全地卸载全局键盘钩子，包含错误处理和重试机制。

  输入参数: 无

  详细说明:
  1. 检查键盘钩子是否已安装(KeyboardHook <> 0)，只有在钩子已安装的情况下才执行卸载操作。
  2. 使用重试机制(最多尝试3次)安全地卸载系统钩子，增强操作可靠性。
  3. 使用 UnhookWindowsHookEx Windows API 函数执行实际的钩子卸载操作。
  4. 如果卸载失败，根据 GetLastError 返回的错误代码提供详细的错误信息。
  5. 对于 ERROR_INVALID_HANDLE 错误，视为钩子已被卸载，直接退出重试循环。
  6. 当卸载成功时，重置 KeyboardHook 变量为 0，表示钩子已被释放。

  注意事项:
  - 重试机制有助于处理临时性的系统资源冲突问题，提高钩子卸载的成功率。
  - 在多次尝试卸载后，如果仍然失败，将显示详细的错误消息对话框。
  - 错误消息根据 Windows 系统错误代码进行分类，便于诊断问题原因。
  - 此过程在应用程序退出前(FormDestroy 事件)会被自动调用，确保系统资源被正确释放。

  修改记录:
  2025-04-16 - 创建过程
  2025-04-18 - 添加重试机制，提高卸载可靠性。完善错误处理，增加详细的错误代码解释。
}
procedure UninstallKeyboardHook;
const
  MAX_TRY = 3;                                      // 最大重试次数
  RETRY_DELAY_MS = 100;                             // 每次重试的延迟时间(毫秒)
var
  TryCount: Integer;                                // 当前尝试次数计数器
  ErrorCode: DWORD;                                 // 存储Windows API返回的错误代码
  Success: Boolean;                                 // 标记卸载操作是否成功的标志
  ErrorMsg: string;                                 // 存储格式化后的错误信息文本
begin
  // 仅在钩子已安装时执行卸载操作
  if KeyboardHook <> 0 then
  begin
    TryCount := 0;                                  // 重置尝试计数器
    ErrorCode := 0;                                 // 初始化错误代码，从逻辑的角度无此必要。
    // 使用重试机制尝试卸载钩子
    repeat
      Success := UnhookWindowsHookEx(KeyboardHook); // 调用Windows API卸载钩子
      if not Success then                           // 如果卸载失败
      begin
        ErrorCode := GetLastError;                  // 获取系统错误代码
        // 如果错误是句柄无效，说明钩子可能已被卸载，直接退出循环
        if ErrorCode = ERROR_INVALID_HANDLE then
          Break;                                    
        Inc(TryCount);                              // 增加尝试计数
        Sleep(RETRY_DELAY_MS);                      // 延迟一段时间后再次尝试，避免资源冲突。
      end;
    until Success or (TryCount >= MAX_TRY);
    
    // 处理卸载结果
    if Success then
      KeyboardHook := 0                             // 如果成功卸载，重置钩子句柄变量。
    else
    begin
      // 如果卸载失败，根据错误代码提供详细的错误信息
      case ErrorCode of
        ERROR_INVALID_HANDLE:
          ErrorMsg := '卸载键盘钩子失败，句柄无效。';
        ERROR_ACCESS_DENIED:
          ErrorMsg := '卸载键盘钩子失败，权限不足，访问被拒绝。';
        ERROR_INVALID_PARAMETER:
          ErrorMsg := '卸载键盘钩子失败，参数无效。';
        ERROR_HOOK_NOT_INSTALLED:
          ErrorMsg := '卸载键盘钩子失败，钩子未安装。';
        ERROR_HOOK_NEEDS_HMOD:
          ErrorMsg := '卸载键盘钩子失败，钩子需要模块句柄。';
        else
          ErrorMsg := '卸载键盘钩子失败，错误代码：' + IntToStr(ErrorCode);
      end;
      // 显示错误信息对话框，通知用户卸载失败。
      MessageBox(
        0,
        PChar('卸载键盘钩子失败，错误代码：' + IntToStr(ErrorCode)),
        '错误警告',
        MB_ICONERROR or MB_OK
      );
    end;
  end;
end;

{
  函数名: VirtualKeyToString
  功能: 将Windows虚拟键码(Virtual Key Code)转换为人类可读的字符串表示。

  参数:
    VirtualKey: DWORD - Windows 定义的虚拟键码，如 VK_ESCAPE, VK_F1 等。

  返回值:
    string - 虚拟键的可读文本表示，如"Esc", "F1", "A"等。

  详细说明:
  该函数通过四种不同的方法依次尝试将虚拟键码转换为可读字符串:
  1. 对于字母A-Z和数字0-9直接转换为对应字符。
  2. 使用预定义的特殊键映射表查找常用特殊键(如F1-F12, 方向键等)。
  3. 调用Windows API获取其他按键名称。
  4. 如果以上方法均失败，返回键码的十六进制表示。

  注意事项：
  - 目前该代码只考虑了101键盘布局的虚拟键码。
  - 如果需要支持其他键盘布局：
    1. 扩展 SPECIAL_KEYS 数组，添加其他键的映射。
    2. 使用 GetKeyboardLayout 和 MapVirtualKeyEx 函数替代当前的键码映射方法。

  修改记录：
  2025-04-16 - 创建函数
  2025-04-18 - 添加对VK_SNAPSHOT 和 VK_OEM_X的支持。
  2025-04-18 - 完善注释
}
function VirtualKeyToString(VirtualKey: DWORD): string;
const
  // 预定义特殊键映射表，包含59个特殊键的虚拟键码和对应名称。
  SPECIAL_KEYS: array[0..58] of record
    VK: DWORD;    // 虚拟键码
    Name: string; // 对应的显示名称
  end = (
    (VK: VK_F1; Name: 'F1'),
    (VK: VK_F2; Name: 'F2'),
    (VK: VK_F3; Name: 'F3'),
    (VK: VK_F4; Name: 'F4'),
    (VK: VK_F5; Name: 'F5'),
    (VK: VK_F6; Name: 'F6'),
    (VK: VK_F7; Name: 'F7'),
    (VK: VK_F8; Name: 'F8'),
    (VK: VK_F9; Name: 'F9'),
    (VK: VK_F10; Name: 'F10'),
    (VK: VK_F11; Name: 'F11'),
    (VK: VK_F12; Name: 'F12'),
    (VK: VK_OEM_1; Name: ';:'),
    (VK: VK_OEM_2; Name: '/?'),
    (VK: VK_OEM_3; Name: '`~'),
    (VK: VK_OEM_4; Name: '[{'),
    (VK: VK_OEM_5; Name: '\|'),
    (VK: VK_OEM_6; Name: ']}'),
    (VK: VK_OEM_7; Name: '''"'),
    (VK: VK_OEM_COMMA; Name: ',<'),
    (VK: VK_OEM_PERIOD; Name: '.>'),
    (VK: VK_OEM_MINUS; Name: '-_'),
    (VK: VK_OEM_PLUS; Name: '=+'),
    (VK: VK_ESCAPE; Name: 'Esc'),
    (VK: VK_TAB; Name: 'Tab'),
    (VK: VK_CAPITAL; Name: 'CapsLock'),
    (VK: VK_SPACE; Name: 'Space'),
    (VK: VK_BACK; Name: 'Backspace'),
    (VK: VK_RETURN; Name: 'Enter'),
    (VK: VK_APPS; Name: 'Apps'),
    (VK: VK_SNAPSHOT; Name: 'PrintScreen'),
    (VK: VK_SCROLL; Name: 'ScrollLock'),
    (VK: VK_PAUSE; Name: 'Pause'),
    (VK: VK_INSERT; Name: 'Insert'),
    (VK: VK_DELETE; Name: 'Del'),
    (VK: VK_HOME; Name: 'Home'),
    (VK: VK_END; Name: 'End'),
    (VK: VK_PRIOR; Name: 'PgUp'),
    (VK: VK_NEXT; Name: 'PgDn'),
    (VK: VK_LEFT; Name: '←'),
    (VK: VK_RIGHT; Name: '→'),
    (VK: VK_UP; Name: '↑'),
    (VK: VK_DOWN; Name: '↓'),
    (VK: VK_NUMLOCK; Name: 'NumLock'),
    (VK: VK_DIVIDE; Name: '/'),
    (VK: VK_MULTIPLY; Name: '*'),
    (VK: VK_SUBTRACT; Name: '-'),
    (VK: VK_ADD; Name: '+'),
    (VK: VK_DECIMAL; Name: '.'),
    (VK: VK_NUMPAD0; Name: 'NumPad0'),
    (VK: VK_NUMPAD1; Name: 'NumPad1'),
    (VK: VK_NUMPAD2; Name: 'NumPad2'),
    (VK: VK_NUMPAD3; Name: 'NumPad3'),
    (VK: VK_NUMPAD4; Name: 'NumPad4'),
    (VK: VK_NUMPAD5; Name: 'NumPad5'),
    (VK: VK_NUMPAD6; Name: 'NumPad6'),
    (VK: VK_NUMPAD7; Name: 'NumPad7'),
    (VK: VK_NUMPAD8; Name: 'NumPad8'),
    (VK: VK_NUMPAD9; Name: 'NumPad9')
  );
var
  I: Integer;                       // 用于遍历特殊键数组的索引
  ScanCode: UINT;                   // 存储扫描码，用于 Windows API 调用。
  KeyNameBuf: array[0..31] of Char; // 存储 Windows API 返回的键名。
begin
  // 1. 1. 对于字母(A-Z)和数字(0-9)键，直接返回对应字符。
  if CharInSet(Char(VirtualKey), ['A'..'Z', '0'..'9']) then
    Exit(Char(VirtualKey));

  // 2. 在预定义特殊键映射表中查找匹配项。
  for I := Low(SPECIAL_KEYS) to High(SPECIAL_KEYS) do
    if SPECIAL_KEYS[I].VK = VirtualKey then
      Exit(SPECIAL_KEYS[I].Name);
  
  // 3. 如果在预定义表中未找到，则调用Windows API获取键名。
  // MapVirtualKey 将虚拟键码转换为扫描码。
  ScanCode := MapVirtualKey(VirtualKey, MAPVK_VK_TO_VSC); 
  // 如果成功获取扫描码，则调用GetKeyNameText获取本地化的键名。
  if (ScanCode > 0) and (GetKeyNameText(ScanCode shl 16, KeyNameBuf, SizeOf(KeyNameBuf)) > 0) then
    Exit(KeyNameBuf);
  
  // 4. 如果以上所有方法都失败，返回键码的十六进制表示。
  // 格式为"VK_#XX"，其中XX是虚拟键码的十六进制表示。
  Result := 'VK_#' + IntToHex(VirtualKey, 2);
end;

{
  函数名: GetWindowTitle
  功能: 获取指定窗口句柄的标题文本。

  参数:
    WindowHandle: HWND - 要查询的窗口句柄

  返回值:
    string - 窗口的标题文本，如果窗口没有标题或句柄无效则返回空字符串。

  详细说明:
  该函数通过调用 Windows API GetWindowText 获取指定窗口的标题文本。
  常用于用户界面反馈，可以向用户显示哪个窗口当前拥有焦点或注册了特定快捷键。

  注意事项:
  - 某些系统窗口或特殊程序可能没有标题文本。
  - 对于没有响应的应用程序，GetWindowText 可能无法返回正确的窗口标题。
  - 对于更复杂的窗口信息获取，可以考虑配合使用 GetClassName 等 API 函数。

  修改记录:
  2025-04-21 - 创建函数
}
function GetWindowTitle(WindowHandle: HWND): string;
const
  MAX_TITLE_LENGTH = 1024;                        // 定义窗口标题的最大长度
var
  Buffer: array[0..MAX_TITLE_LENGTH - 1] of Char; // 用于存储窗口标题的缓冲区
  Length: Integer;                                // 存储获取到的标题长度
begin
  Result := '';                                   // 初始化返回值为空字符串
  
  // 检查窗口句柄是否有效
  if WindowHandle <> 0 then
  begin
    // 调用 Windows API 获取窗口标题
    Length := GetWindowText(WindowHandle, Buffer, MAX_TITLE_LENGTH);
    
    // 如果成功获取到标题(长度大于0)，则将缓冲区内容转换为Delphi字符串。
    if Length > 0 then
      Result := Buffer;
  end;
end;

{
  函数名: GetProcessName
  功能: 获取指定进程ID对应的可执行文件名称(不含路径)。

  参数:
    ProcessId: DWORD - 要查询的进程标识符(PID)

  返回值:
    string - 进程的可执行文件名，若无法获取则返回空字符串

  详细说明:
  该函数通过以下步骤获取进程的可执行文件名:
  1. 使用有限权限(PROCESS_QUERY_LIMITED_INFORMATION)打开指定进程。
  2. 调用QueryFullProcessImageName API获取进程的完整可执行文件路径。
  3. 从完整路径中提取文件名部分(不含路径)作为返回值。

  注意事项:
  - 该函数使用 Vista 及更高版本引入的 API，在较旧 Windows 版本(如XP)上不可用。
  - 使用最小权限方式打开进程，降低被访问控制拒绝的可能性。
  - 某些系统进程或受保护进程可能无法被正常访问，此时将返回空字符串。
  - 函数确保无论成功与否都会正确关闭进程句柄，防止资源泄漏。

  性能考虑:
  - 对于频繁调用的场景，可考虑实现结果缓存以提升性能。

  修改记录:
  2025-04-18 - 创建函数
  2025-04-21 - 使用 QueryFullProcessImageName 替换旧方法，降低权限需求。
}
function GetProcessName(ProcessId: DWORD): string;
var
  hProcess: THandle;                    // 存储打开的进程句柄
  dwSize: DWORD;                        // 用于API调用时指示缓冲区大小和返回实际路径长度
  Path: array[0..MAX_PATH-1] of Char;   // 用于存储进程完整路径的字符缓冲区
begin
  // 初始化返回值为空字符串，确保在任何出错情况下都有默认返回值。
  Result := '';

  // 尝试打开目标进程，只请求最小权限以提高成功率。
  // PROCESS_QUERY_LIMITED_INFORMATION 是 Vista+ 系统提供的低权限访问标志。
  // False 参数表示子进程不会继承此句柄。
  hProcess := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, ProcessId);

  // 仅当成功获取进程句柄时才继续执行
  if hProcess <> 0 then
  try
    // 设置初始缓冲区大小为MAX_PATH常量值(通常为260)
    // 此变量是输入/输出参数，API调用后会被修改为实际路径长度。
    dwSize := MAX_PATH;
    if QueryFullProcessImageName(hProcess, 0, Path, dwSize) then
    begin
      // API调用成功，从完整路径中提取文件名部分。
      // ExtractFileName是Delphi 提供的路径处理函数
      Result := ExtractFileName(Path);
    end;
  finally
    // 确保无论成功与否都释放进程句柄，防止资源泄漏。
    CloseHandle(hProcess);
  end;
  // 如果无法打开进程(如权限不足)，直接返回空字符串。
end;


{
  函数名: KeyboardProc
  功能: Windows低级键盘钩子回调函数，用于捕获键盘事件并检测快捷键组合是否已被系统中其他程序注册。

  参数:
    nCode: Integer - 钩子代码，决定钩子如何处理消息。
    wParam: WPARAM - 消息类型，如WM_KEYDOWN、WM_KEYUP等。
    lParam: LPARAM - 指向KBDLLHOOKSTRUCT结构的指针，包含按键的详细信息。

  返回值:
    LRESULT - 返回 CallNextHookEx的 结果，以确保消息正确传递给钩子链中的其他钩子。

  详细说明:
  1. 当按下组合键(含修饰键如Shift/Ctrl/Alt/Win)时，尝试使用 RegisterHotKey API 临时注册该组合：
     - 如果注册成功：表明该快捷键组合当前未被任何程序占用，随后立即注销该注册
     - 如果注册失败：表明该快捷键已被其他程序注册，尝试识别可能的注册者

  2. 对于已注册的快捷键，使用启发式方法识别可能的注册程序：
     - 获取当前前台窗口所属的进程
     - 获取该进程的可执行文件名作为可能的快捷键注册者

  3. 将检测到的快捷键组合及其状态信息异步更新到界面

  注意事项:
  - 必须始终调用CallNextHookEx确保钩子链正常工作，否则可能导致系统键盘事件处理异常。
  - 使用 TThread.Queue 异步更新UI ，避免在钩子回调中直接操作UI导致的线程安全问题。
  - 启发式识别注册者并非 100% 准确，仅作为参考信息提供给用户。

  修改记录:
  2025-04-16 - 创建函数
  2025-04-18 - 完善快捷键检测逻辑，增加注册者识别功能
}
function KeyboardProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  HookStruct: PKBDLLHOOKSTRUCT;         // 指向键盘事件结构的指针，包含按键详细信息
  Modifiers: TModifierKeys;             // 存储当前按下的所有修饰键(Shift/Ctrl/Alt/Win)
  VirtualKey: DWORD;                    // 存储当前按下的虚拟键码
  KeyName: string;                      // 虚拟键的可读文本表示
  ModifiersStr: string;                 // 修饰键的文本表示(如"Ctrl+Shift+")
  InfoStr: string;                      // 用于UI显示的状态信息文本
  ErrorCode: DWORD;                     // 存储API调用返回的错误代码
  ModifiersFlag: UINT;                  // 转换为Windows API需要的修饰键标志
  CanRegister: Bool;                    // 标记是否可以成功注册快捷键
  ForegroundWindow: HWND;               // 存储当前前台窗口句柄
  ProcessId: DWORD;                     // 存储前台窗口所属的进程ID
  ProcessName: string;                  // 存储进程的可执行文件名
  WindowsTitle: string;                 // 存储前台窗口的标题文本
begin
  // 必须首先检查nCode有效性。
  // 当nCode < 0时，Windows要求钩子过程必须将消息传递给链中的下一个钩子。
  // 不这样做会破坏系统钩子链，可能导致其他应用程序无法接收键盘事件。
  if nCode < 0 then
  begin
    Result := CallNextHookEx(0, nCode, wParam, lParam);
    Exit;
  end;

  // 只处理按键按下事件
  if (wParam = WM_KEYDOWN) or (wParam = WM_SYSKEYDOWN) then
  begin
    // 初始化修饰键集合为空，然后检测所有可能的修饰键当前是否被按下。
    // GetKeyState 返回负值表示键被按下
    Modifiers := [];
    if GetKeyState(VK_SHIFT) < 0 then
      Include(Modifiers, mkShift);
    if GetKeyState(VK_CONTROL) < 0 then
      Include(Modifiers, mkCtrl);
    if GetKeyState(VK_MENU) < 0 then
      Include(Modifiers, mkAlt);
    if (GetKeyState(VK_LWIN) < 0) or (GetKeyState(VK_RWIN) < 0) then
      Include(Modifiers, mkWin);

    // 只有当至少有一个修饰键按下时才进行快捷键检测。
    // 这是为了避免检测单个普通按键，提高程序性能。
    if Modifiers <> [] then
    begin
      // 从 lParam 参数中获取键盘事件详细信息。
      HookStruct := PKBDLLHOOKSTRUCT(lParam);
      VirtualKey := HookStruct.vkCode;  // 提取虚拟键码
      // 排除单独按下的修饰键情况，只检查修饰键+其他键的组合
      if not (VirtualKey in [VK_LSHIFT, VK_RSHIFT, VK_LCONTROL, VK_RCONTROL, VK_LMENU, VK_RMENU, VK_LWIN, VK_RWIN]) then
      begin
        // 将虚拟键码转换为可读字符串表示
        KeyName := VirtualKeyToString(VirtualKey);
        // 初始化修饰键文本和标志，用于UI显示和API调用。
        ModifiersStr := '';
        ModifiersFlag := 0;
        // 处理各种修饰键，构建文本表示和位标志。
        if mkShift in Modifiers then
        begin
          ModifiersStr := ModifiersStr + 'Shift+';
          ModifiersFlag := ModifiersFlag or MOD_SHIFT;
        end;
        if mkCtrl in Modifiers then
        begin
          ModifiersStr := ModifiersStr + 'Ctrl+';
          ModifiersFlag := ModifiersFlag or MOD_CONTROL;
        end;
        if mkAlt in Modifiers then
        begin
          ModifiersStr := ModifiersStr + 'Alt+';
          ModifiersFlag := ModifiersFlag or MOD_ALT;
        end;
        if mkWin in Modifiers then
        begin
          ModifiersStr := ModifiersStr + 'Win+';
          ModifiersFlag := ModifiersFlag or MOD_WIN;
        end;
        // 确保主窗体已创建并分配了窗口句柄，这是注册快捷键所必需的。
        if (frmMain <> nil) and (frmMain.HandleAllocated) then
        begin
          // 尝试临时注册快捷键组合，ID使用修饰键和虚拟键的组合确保唯一性。
          CanRegister := RegisterHotKey(frmMain.Handle, (ModifiersFlag shl 8) or VirtualKey, ModifiersFlag, VirtualKey);
          
          // 如果注册失败，说明快捷键可能已被其他程序注册。
          if not CanRegister then
          begin
            ErrorCode := GetLastError;
            // 特别处理快捷键已被注册的情况，查找是哪个进程注册了该快捷键。
            if ErrorCode = ERROR_HOTKEY_ALREADY_REGISTERED then
            begin
              // Windows API不提供直接查询哪个进程注册了特定快捷键的方法
              // 这里使用的是启发式方法：假设前台窗口的进程可能是快捷键注册者
              // 虽然这不是100%准确，但在大多数情况下可以为用户提供有用的信息
              // 用户可以根据提示了解哪些应用程序可能在使用该快捷键
              // 这里的Sleep是为了让注册的快捷键有时间生效，未来尝试修改为非阻塞的方式。
              Sleep(1000);

              // 获取当前前台窗口，可能是快捷键注册者。
              ForegroundWindow := GetForegroundWindow;
              if ForegroundWindow = 0 then
                InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 可能已被注册，但无法猜测注册者。'
              else
              begin
                // 获取前台窗口所属的进程ID
                GetWindowThreadProcessId(ForegroundWindow, @ProcessId);
                // 尝试获取进程的可执行文件名
                ProcessName := GetProcessName(ProcessId);
                // 获取窗口标题
                WindowsTitle := GetWindowTitle(ForegroundWindow);
                if ProcessName <> '' then
                  InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 已被注册，注册者： ' + ProcessName + ' 。'
                else
                  InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 已被注册，注册者： ' + IntToHex(ProcessId, 8) + ' 。';
                InfoStr := InfoStr + '窗口标题： ' + WindowsTitle + ' 。';
              end;
            end
            else
            begin
              // 处理其他类型的错误，提供更具体的错误信息。
              case ErrorCode of
                ERROR_INVALID_PARAMETER:
                  InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 注册失败，参数无效。';
                ERROR_ACCESS_DENIED:
                  InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 注册失败，权限不足，访问被拒绝。';
                else
                  InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 注册失败，错误代码：' + IntToStr(ErrorCode) + '。';
              end;
            end;
          end
          else
          begin
            // 如果快捷键注册成功，表明此快捷键未被其他程序注册。
            // 注销刚才注册的快捷键，以避免不必要地占用系统快捷键资源。
            UnregisterHotKey(frmMain.Handle, (ModifiersFlag shl 8) or VirtualKey);
            InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 未被注册。';
          end;
        end
        else
          // 如果窗体句柄无效，无法执行快捷键注册操作
          InfoStr := '快捷键： ' + ModifiersStr + KeyName + ' 注册失败，窗口句柄无效。';
        
        // 使用线程队列安全地更新UI，避免在钩子回调直接更新UI可能导致的线程问题。
        // 这是因为键盘钩子可能在与UI线程不同的线程上执行。
        TThread.Queue(
          nil,
          procedure
          begin
            frmMain.lblHotKey.Caption := ModifiersStr + KeyName;
            frmMain.lblInfo.Caption := InfoStr;
          end
        );
      end;
    end;
  end;

  // 必须调用CallNextHookEx将消息传递给钩子链中的下一个钩子
  // 这确保系统钩子链的完整性，其他应用程序也能正常接收键盘事件
  Result := CallNextHookEx(0, nCode, wParam, lParam);
end;

{
  过程名: InstallKeyboardHook
  功能: 安装全局键盘低级钩子，用于监控系统中所有键盘事件

  输入参数: 无

  详细说明:
  1. 首先检查键盘钩子是否已安装(KeyboardHook <> 0)，如已安装则先卸载现有钩子。
  2. 使用重试机制(最多尝试3次)安全地安装系统钩子，提高操作可靠性。
  3. 使用SetWindowsHookEx Windows API函数执行实际的钩子安装，
     指定WH_KEYBOARD_LL作为钩子类型，以捕获低级键盘事件。
  4. 指定当前应用程序模块句柄和线程ID 0，确保钩子可以捕获全局键盘事件。
  5. 如果安装失败，根据GetLastError返回的错误代码提供详细错误信息。
  6. 通过TThread.Queue异步更新UI，避免线程安全问题。

  注意事项:
  - 重试机制有助于处理临时性系统资源冲突，提高钩子安装成功率。
  - WH_KEYBOARD_LL钩子需要持续运行才能接收事件，会对系统性能有轻微影响。
  - 此过程在应用程序启动时(FormShow事件)会被自动调用。
  - 应用程序关闭时必须卸载钩子以避免系统资源泄漏。
  - 钩子安装失败时，会在界面显示详细的错误信息，颜色为红色警示。

  修改记录:
  2025-04-16 - 创建过程
  2025-04-18 - 添加重试机制，提高安装可靠性。完善错误处理，增加详细的错误代码解释。
}
procedure InstallKeyboardHook;
const
  MAX_TRY = 3;                          // 安装钩子的最大尝试次数，避免无限循环。
  RETRY_DELAY_MS = 100;                 // 尝试安装失败后延迟时间(毫秒)，给系统资源释放的时间。
var
  TryCount: Integer;                    // 当前尝试计数，用于跟踪已尝试的次数。
  ErrorCode: DWORD;                     // 存储Windows API调用失败时的错误代码
  ErrorMsg: string;                     // 根据错误代码生成的友好错误信息
begin
  // 如果钩子已安装，先卸载它，确保不会重复安装造成资源泄漏。
  if KeyboardHook <> 0 then
    UninstallKeyboardHook;
  
  TryCount := 0;                        // 初始化尝试计数为0
  repeat
    // 调用Windows API安装低级键盘钩子：
    // - WH_KEYBOARD_LL参数指定为低级键盘钩子
    // - @KeyboardProc是钩子回调函数的指针
    // - GetModuleHandle(nil)获取当前应用程序模块句柄
    // - 0表示钩子应当监控所有线程(全局钩子)
    KeyboardHook := SetWindowsHookEx(WH_KEYBOARD_LL, @KeyboardProc, GetModuleHandle(nil), 0);
    // 安装成功，句柄值不为0，跳出循环。
    if KeyboardHook <> 0 then
      Break;
    
    Inc(TryCount);                      // 增加尝试次数
    Sleep(RETRY_DELAY_MS);              // 延迟一段时间再次尝试，避免资源冲突。
  until TryCount >= MAX_TRY;            // 尝试直到达到最大次数。
  
  // 如果经过多次尝试后钩子仍未安装成功，处理错误信息。
  if KeyboardHook = 0 then
  begin
    ErrorCode := GetLastError;          // 获取Windows系统错误代码
    // 根据不同的错误代码生成具体的错误信息，便于用户理解问题原因。
    case ErrorCode of
      ERROR_INVALID_PARAMETER:
        ErrorMsg := '安装键盘钩子失败，参数无效。';
      ERROR_ACCESS_DENIED:
        ErrorMsg := '安装键盘钩子失败，权限不足，访问被拒绝。';
      ERROR_HOOK_TYPE_NOT_ALLOWED:
        ErrorMsg := '安装键盘钩子失败，钩子类型不允许。';
      ERROR_NOT_ENOUGH_MEMORY:
        ErrorMsg := '安装键盘钩子失败，内存不足。';
      ERROR_INVALID_HOOK_FILTER:
        ErrorMsg := '安装键盘钩子失败，钩子过滤器无效。';
      ERROR_INVALID_HOOK_HANDLE:
        ErrorMsg := '安装键盘钩子失败，钩子句柄无效。';
      else
        ErrorMsg := '安装键盘钩子失败，错误代码：' + IntToStr(ErrorCode);
    end;

    // 使用线程队列安全地更新UI，避免线程安全问题
    // 这是必要的，因为钩子可能在非UI线程上安装
    TThread.Queue(
      nil,
      procedure
      begin
        frmMain.lblInfo.Font.Color := clRed;
        frmMain.lblInfo.Caption := ErrorMsg;
      end
    );
  end;
end;

procedure TfrmMain.btnQuitClick(Sender: TObject);
begin
  Application.Terminate;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  UninstallKeyboardHook;
end;

procedure TfrmMain.FormShow(Sender: TObject);
begin
  InstallKeyboardHook;
end;
  
end.
